package org.dreamwork.jasmine2.restful;

import com.google.gson.GsonBuilder;
import org.dreamwork.jasmine2.restful.annotation.*;
import org.dreamwork.misc.XMLUtil;
import org.dreamwork.secure.*;
import org.dreamwork.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.servlet.ServletContext;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.security.NoSuchAlgorithmException;
import java.util.*;

/**
 * Created with IntelliJ IDEA.
 * User: seth.yang
 * Date: 14-11-17
 * Time: 下午8:33
 *
 */
public class RestfulAPIManager {
    public static final String KEY_API_MAPPING        = "jasmine.restful.api.mapping";
//    public static final String KEY_PRIVATE_KEY_HOLDER = "jasmine.restful.api.private-key";
    private static final Logger logger = LoggerFactory.getLogger (RestfulAPIManager.class);

/*
    private static final Map<String, Class> cache = new HashMap<> ();

    static {
        cache.put ("byte", byte.class);
        cache.put ("int", int.class);
        cache.put ("short", short.class);
        cache.put ("long", long.class);

    }
*/

    private TransformerFactory tf;
    private ServletContext application;
    private GsonBuilder builder = new GsonBuilder ().excludeFieldsWithoutExposeAnnotation ();

    public static Map<String, APIDefinition> load (InputStream in, ServletContext application) throws ParserConfigurationException, IOException, SAXException, TransformerException, IllegalAccessException, ClassNotFoundException, InstantiationException, NoSuchAlgorithmException {
        return new RestfulAPIManager (application).doParse (in);
    }

    public static Map<String, APIDefinition> load (Element root, ServletContext application) throws TransformerException, IOException, SAXException, IllegalAccessException, InstantiationException, ClassNotFoundException, NoSuchAlgorithmException {
        return new RestfulAPIManager (application).parseConfigFile (root);
    }

    private RestfulAPIManager (ServletContext application) {
        this.application = application;
        tf = TransformerFactory.newInstance ();
    }

    private Map<String, APIDefinition> doParse (InputStream in) throws ParserConfigurationException, IOException, SAXException, TransformerException, IllegalAccessException, InstantiationException, ClassNotFoundException, NoSuchAlgorithmException {
        try {
            return parseConfigFile (XMLUtil.parse (in).getDocumentElement ());
        } finally {
            in.close ();
        }
    }

    private Map<String, APIDefinition> parseConfigFile (Element root) throws TransformerException, IOException, SAXException, IllegalAccessException, ClassNotFoundException, InstantiationException, NoSuchAlgorithmException {
        InputStream xsd = getClass ().getResourceAsStream ("resource/restful-api.xsd");
        try {
            XMLUtil.validDocumentByXSD (new StreamSource (xsd), new DOMSource (root));
        } finally {
            xsd.close ();
        }

        SortedMap<String, APIDefinition> map = new TreeMap<> ();
        // global description
        String description = getDescription (root);
        if (!StringUtil.isEmpty (description))
            RestfulAPIContext.description = description;
        String title = getTitle (root);
        if (!StringUtil.isEmpty (title)) {
            RestfulAPIContext.title = title;
        }

        NodeList nl = root.getElementsByTagName ("app-id");
        if (nl != null && nl.getLength () > 0) {
            Element e = (Element) nl.item (0);
            if (e.hasAttribute ("required")) {
                RestfulAPIContext.appIdRequired = "true".equals (e.getAttribute ("required"));
            }
            if (e.hasAttribute ("location")) {
                String location = e.getAttribute ("location");
                try {
                    RestfulAPIContext.location = AppIdLocation.valueOf (location);
                } catch (Exception ex) {
                    logger.warn (ex.getMessage (), ex);
                }
            }
        }

        if (RestfulAPIContext.appIdRequired == null && root.hasAttribute ("app-id")) {
            String use = root.getAttribute ("app-id").trim ();
            RestfulAPIContext.appIdRequired = "required".equals (use);
        }
        if (RestfulAPIContext.location == null && root.hasAttribute ("app-id-location")) {
            String location = root.getAttribute ("app-id-location").trim ();
            try {
                RestfulAPIContext.location = AppIdLocation.valueOf (location);
            } catch (Exception ex) {
                logger.warn (ex.getMessage (), ex);
            }
        }

        NodeList list = root.getElementsByTagName ("secure-context");
        if (list != null && list.getLength () > 0) {
            parseSecureContext ((Element) list.item (0));
        }

        List<ContextParameterDefinition> parameters = parseContextParameter (root);
        if (!parameters.isEmpty ()) {
            for (ContextParameterDefinition p : parameters)
                RestfulAPIContext.addContextParameterName (p);
        }

        list = root.getElementsByTagName ("annotated-class");
        if (list != null && list.getLength () > 0) for (int i = 0; i < list.getLength (); i ++) {
            Element e = (Element) list.item (i);
            parseAnnotatedClasses (e, parameters, map);
        }

        list = root.getElementsByTagName ("api");
        if (list != null && list.getLength () > 0) for (int i = 0; i < list.getLength (); i ++) {
            Element e = (Element) list.item (i);
            parseAPI (e, parameters, map);
        }

        return map;
    }

    private MethodDefinition parseMethod (Element e) throws TransformerException, IOException {
        String type = e.getAttribute ("type").trim ();
        String pattern = e.getAttribute ("pattern").trim ();
        MethodType mt = MethodType.parse (type);
        if (mt == null) {
            logger.warn ("Unknown method type: " + type);
            logger.warn (toString (e));
            logger.warn ("this node will be ignored");
            return null;
        }

        List<ContextParameterDefinition> list = parseContextParameter (e);

        MethodDefinition method = new MethodDefinition ();
        method.pattern = pattern;
        method.setDescription (getDescription (e));
        method.type = mt;
        method.title = e.getAttribute ("title").trim ();
        if (!list.isEmpty ()) for (ContextParameterDefinition p : list) {
            method.addContextParameter (p);
        }

        NodeList nl = e.getElementsByTagName ("result");
        if (nl.getLength () > 0)
            parseResult ((Element) nl.item (0), method);

        String className;
        Class<?> clazz;
        nl = e.getElementsByTagName ("api-parameter");
        if (nl.getLength () > 0) for (int i = 0; i < nl.getLength (); i ++) {
            Element p = (Element) nl.item (i);
            className = p.getAttribute ("type");
            try {
                clazz = Class.forName (className);
                APIParameterDefinition pd = new APIParameterDefinition ();
                pd.setName (p.getAttribute ("name"));
                pd.setClassName (className);
                pd.setType (clazz);
                String desc = getDescription (p);
                if (!StringUtil.isEmpty (desc))
                    pd.setDescription (desc);
                method.addParameter (pd);
            } catch (ClassNotFoundException ex) {
                logger.warn ("class " + className + " not found!");
            }
        }

        if (logger.isDebugEnabled ()) {
            logger.debug ("method load as: " + builder.create ().toJson (method));
        }
        return method;
    }

    private void parseResult (Element parent, MethodDefinition method) {
        NodeList list = parent.getElementsByTagName ("field");
        for (int i = 0; i < list.getLength (); i ++) {
            Element e = (Element) list.item (i);
            ResultFieldDefinition field = new ResultFieldDefinition ();
            field.setDescription (getDescription (e));
            field.setName (e.getAttribute ("name").trim ());
            if (e.hasAttribute ("mandatory")) {
                field.setMandatory (Boolean.valueOf (e.getAttribute ("mandatory").trim ()));
            }
            if (e.hasAttribute ("type")) {
                field.setType (e.getAttribute ("type").trim ());
            }
            method.addResultField (field);
        }
    }

    private String toString (Node node) throws TransformerException, IOException {
        Source source = new DOMSource (node);
        ByteArrayOutputStream baos = new ByteArrayOutputStream ();
        Result result = new StreamResult (baos);
        tf.newTransformer ().transform (source, result);
        return baos.toString ("utf-8");
    }

    private String getByName (Element e, String name) {
        NodeList list = e.getChildNodes ();
        for (int i = 0; i < list.getLength (); i ++) {
            Node node = list.item (i);
            if (node.getNodeType () == Node.ELEMENT_NODE && name.equals (node.getNodeName ())) {
                return node.getTextContent ();
            }
        }

        if (e.hasAttribute (name)) {
            return e.getAttribute (name);
        }

        return null;
    }

    private String getTitle (Element e) {
        return getByName (e, "title");
    }

    private String getDescription (Element e) {
        return getByName (e, "description");
/*
        NodeList nl = e.getChildNodes ();
        for (int i = 0; i < nl.getLength (); i ++) {
            Node node = nl.item (i);
            if (node.getNodeType () == Node.ELEMENT_NODE && "description".equals (node.getNodeName ())) {
                return node.getTextContent ();
            }
        }
        if (e.hasAttribute ("description")) {
            return e.getAttribute ("description").trim ();
        }
        return null;
*/
    }

    private List<ContextParameterDefinition> parseContextParameter (Element parent) throws TransformerException, IOException {
        List<ContextParameterDefinition> list = new ArrayList<> ();
        NodeList children = parent.getChildNodes ();
        for (int i = 0; i < children.getLength (); i ++) {
            Node node = children.item (i);
            if (node.getNodeType () == Node.ELEMENT_NODE && "context-parameters".equals (node.getNodeName ())) {
                Element e = (Element) node;
                NodeList nl = e.getElementsByTagName ("context-parameter");
                for (int j = 0; j < nl.getLength (); j ++) {
                    Element e1 = (Element) nl.item (j);
                    String name = e1.getAttribute ("name");
                    if (StringUtil.isEmpty (name)) {
                        logger.warn ("Attribute [name] of element [context-parameter] can't be empty");
                        logger.warn (toString (e1));
                        logger.warn ("this node will be ignored");
                        continue;
                    }

                    boolean required = false;
                    if (e1.hasAttribute ("required")) {
                        required = Boolean.valueOf (e1.getAttribute ("required").trim ());
                    }

                    String description = getDescription (e1);

                    ContextParameterDefinition p = new ContextParameterDefinition (name.trim (), required);
                    if (!StringUtil.isEmpty (description))
                        p.setDescription (description);
                    list.add (p);
                }
            }
        }
        return list;
    }

    private void parseAnnotatedClasses (Element e, List<ContextParameterDefinition> parameters, SortedMap<String, APIDefinition> map) {
        String className = e.getAttribute ("class").trim ();
        if (logger.isDebugEnabled ())
            logger.debug ("Trying to parse class: " + className);
        Class type;
        try {
            type = Class.forName (className);

            @SuppressWarnings ("unchecked")
            RestfulAPI annoAPI = (RestfulAPI) type.getAnnotation (RestfulAPI.class);
            if (annoAPI == null) {
                logger.warn ("not a restful api implement!");
                return;
            }

            if (logger.isDebugEnabled ())
                logger.debug ("find a restful api matching pattern: " + annoAPI.pattern ());
            APIDefinition api = new APIDefinition ();
            api.className = className;
            api.type = type;
            api.pattern = annoAPI.pattern ();
            api.title = annoAPI.title ();
            api.forceEncrypt = annoAPI.forceEncrypt ();
            api.forceJson = annoAPI.forceJson ();

            if (!StringUtil.isEmpty (annoAPI.description ()))
                api.description = annoAPI.description ();
            else if (type.isAnnotationPresent (Description.class)) {
                @SuppressWarnings ("unchecked")
                Description desc = (Description) type.getAnnotation (Description.class);
                api.description = desc.value ();
            }

            for (ContextParameterDefinition cpd : parameters)
                api.contextParameters.put (cpd.getName (), cpd);

            findContextParameterFields (type, api);
            Collection<ContextParameterDefinition> c = api.getContextParameters ();

            for (Method method : type.getMethods ()) {
                // ignore java.lang.Object's methods.
                if (method.getDeclaringClass () == Object.class) continue;

                if (method.isAnnotationPresent (BeforeExecute.class)) {
                    api.beforeApiExecuteMethods.add (method);
                } else if (method.isAnnotationPresent (RestfulAPI.class)) {
                    annoAPI = method.getAnnotation (RestfulAPI.class);
                    MethodDefinition md = parseMethod (annoAPI, method, c);
                    md.forceEncrypt = annoAPI.forceEncrypt () || api.forceEncrypt;
                    md.forceJson = annoAPI.forceJson () || api.forceJson;
                    md.skipBeforeExecute = annoAPI.skipBeforeExecute ();
                    api.addMethod (md);
                }
            }
            map.put (api.getPattern (), api);
            if (logger.isDebugEnabled ())
                logger.debug ("api [" + api.getPattern () + "] load as: " + builder.create ().toJson (api));
        } catch (ClassNotFoundException ex) {
            logger.warn ("class " + className + " not found! Ignore it!");
        } catch (Exception ex) {
            logger.warn (ex.getMessage (), ex);
        }
    }

    private void findContextParameterFields (Class type, APIDefinition api) {
        List<Field> fields = new ArrayList<> ();
        findAllFields (type, fields);
        for (Field f : fields) {
            ContextParameter cp = f.getAnnotation (ContextParameter.class);
            if (cp != null) {
                boolean required = cp.required ();
                String name = StringUtil.isEmpty (cp.name ()) ? f.getName () : cp.name ().trim ();
                Class<?> ct = cp.type () == Void.class ? f.getType () : cp.type ();
                if (!api.contextParameters.containsKey (name) || required) {
                    ContextParameterDefinition cpd = new ContextParameterDefinition (name, required);
                    if (!StringUtil.isEmpty (cp.description ()))
                        cpd.description = cp.description ();
                    cpd.type = ct;
                    cpd.typeName = ct.getCanonicalName ();
                    api.contextParameters.put (name, cpd);
                }
            }
        }
    }

    private void findAllFields (Class type, List<Field> list) {
        list.addAll (Arrays.asList (type.getDeclaredFields ()));
        if (type.getSuperclass () != null)
            findAllFields (type.getSuperclass (), list);
    }

    private MethodDefinition parseMethod (RestfulAPI anApi, Method method, Collection<ContextParameterDefinition> c) throws NoSuchFieldException {
        String name = method.getName ();
        MethodDefinition md = new MethodDefinition ();
        String pattern = anApi.pattern ();
        if (StringUtil.isEmpty (pattern))
            pattern = name;
        md.setName (name);
        md.pattern = pattern;
        md.type = anApi.method ();
        md.title = anApi.title ();

        if (!c.isEmpty ()) for (ContextParameterDefinition cpd : c) {
            md.contextParameters.put (cpd.name, cpd);
        }
        appendContextParameters (anApi, md, method);

        Description desc = method.getAnnotation (Description.class);
        if (desc != null && !StringUtil.isEmpty (desc.value ()))
            md.setDescription (desc.value ().trim ());
        else if (!StringUtil.isEmpty (anApi.description ()))
            md.setDescription (anApi.description ().trim ());
        if (anApi.result ().length > 0) for (ResultField rf : anApi.result ()) {
            ResultFieldDefinition rfd = new ResultFieldDefinition ();
            rfd.setType (rf.type ()).setName (rf.name ()).setMandatory (rf.mandatory ());
            if (!StringUtil.isEmpty (rf.description ()))
                rfd.setDescription (rf.description ());
            md.addResultField (rfd);
        }

        appendApiParameters (anApi, md, method);
        return md;
    }

    private void appendApiParameters (RestfulAPI anApi, MethodDefinition md, Method method) {
        if (anApi.apiParameters ().length > 0) {
            for (int i = 0; i < anApi.apiParameters ().length; i ++) {
                APIParameter ap = anApi.apiParameters () [i];
                APIParameterDefinition pd = new APIParameterDefinition ();
                pd.setName (ap.name ());
                pd.setNullable (ap.nullable ());
                pd.setDescription (ap.description ());
                if (ap.type () == Void.class) {
                    Class<?> pt = method.getParameterTypes () [i];
                    pd.setClassName (pt.getCanonicalName ());
                    pd.setType (pt);
                } else {
                    pd.setClassName (ap.type ().getCanonicalName ());
                    pd.setType (ap.type ());
                }
                md.addParameter (pd);
            }
        } else {
            Annotation[][] parameters = method.getParameterAnnotations ();
            if (parameters != null && parameters.length > 0) {
                for (int i = 0; i < parameters.length; i ++) {
                    Annotation[] ans = parameters [i];
                    if (ans.length > 0) {
                        APIParameter ap = (APIParameter) ans [0];
                        if (ap.type () == Void.class) {
                            Class<?> pt = method.getParameterTypes () [i];
                            APIParameterDefinition pd = new APIParameterDefinition ();
                            pd.setName (ap.name ());
                            pd.setType (pt);
                            pd.setClassName (pt.getCanonicalName ());
                            pd.setDescription (ap.description ());
                            pd.setNullable (ap.nullable ());
                            md.addParameter (pd);
                        } else {
                            appendAPIParameter (ap, md);
                        }
                    }
                }
            }
        }
    }

    private void appendContextParameters (RestfulAPI anApi, MethodDefinition md, Method method) throws NoSuchFieldException {
        if (anApi.contextParameters ().length > 0) for (ContextParameter cp : anApi.contextParameters ()) {
            Class<?> type = cp.type ();
            ContextParameterDefinition cpd;
            if (type == Void.class) {
                if (md.contextParameters.containsKey (cp.name ())) {
                    ContextParameterDefinition old = md.contextParameters.get (cp.name ());
                    cpd = new ContextParameterDefinition (old.name, cp.required ());
                    cpd.type = old.type;
                    cpd.typeName = cpd.type.getCanonicalName ();
                } else {
                    Class<?> ownerType = method.getDeclaringClass ();
                    Field field = ownerType.getField (cp.name ());
                    if (field == null)
                        throw new NoSuchFieldException (cp.name ());
                    cpd = new ContextParameterDefinition (cp.name (), cp.required ());
                    cpd.type = field.getType ();
                    cpd.typeName = cpd.type.getCanonicalName ();
                }
            } else {
                cpd = new ContextParameterDefinition (cp.name (), cp.required ());
            }
            if (!StringUtil.isEmpty (cp.description ()))
                cpd.description = cp.description ();
            md.contextParameters.put (cpd.name, cpd);
        }
    }

    private void appendAPIParameter (APIParameter ap, MethodDefinition method) {
        APIParameterDefinition pd = new APIParameterDefinition ();
        pd.setClassName (ap.type ().getCanonicalName ());
        pd.setType (ap.type ());
        pd.setName (ap.name ());
        pd.setDescription (ap.description ());
        method.addParameter (pd);
    }

    private void parseAPI (Element e, List<ContextParameterDefinition> parameters, SortedMap<String, APIDefinition> map) throws TransformerException, IOException {
        String pattern = e.getAttribute ("pattern").trim ();
        String className = e.getAttribute ("class").trim ();
        Class<?> type;
        try {
            className = className.trim ();
            type = Class.forName (className);
        } catch (ClassNotFoundException cnfe) {
            logger.warn ("class " + className + " not found!");
            logger.warn (toString (e));
            logger.warn ("this node will be ignored");
            return;
        }

        parameters.addAll (parseContextParameter (e));

        APIDefinition api = new APIDefinition ();
        api.pattern = pattern.trim ();
        api.setClassName (className);
        api.setType (type);
        api.description = getDescription (e);
        api.title = e.getAttribute ("title").trim ();
        NodeList list = e.getElementsByTagName ("method");
        if (list != null && list.getLength () > 0) for (int i = 0 ; i < list.getLength (); i ++) {
            Element method = (Element) list.item (i);
            MethodDefinition md = parseMethod (method);
            if (md != null) api.addMethod (md);
        }
        if (!parameters.isEmpty ()) for (ContextParameterDefinition p : parameters) {
            api.addContextParameter (p);
        }

        if (logger.isDebugEnabled ())
            logger.debug ("api[" + pattern + "] load as " + builder.create ().toJson (api));

        if (map.containsKey (pattern))
            logger.warn ("Pattern [] " + pattern + " exists, and the old one will be replaced");

        map.put (api.getPattern (), api);
    }

    private void parseSecureContext (Element e) throws IllegalAccessException, InstantiationException, ClassNotFoundException, NoSuchAlgorithmException {
        SecureContext context = new SecureContext ();
        KeyFetcherFactory factory;
        if (e.hasAttribute ("factory")) {
            String className = e.getAttribute ("factory");
            if (logger.isDebugEnabled ()) {
                logger.debug ("found key fetcher factory: " + className);
            }
            @SuppressWarnings ("unchecked")
            Class<KeyFetcherFactory> type = (Class<KeyFetcherFactory>) Class.forName (className);
            factory = type.newInstance ();
            if (RestfulAPIKeyFetcherFactory.class.isAssignableFrom (type)) {
                ((RestfulAPIKeyFetcherFactory) factory).setApplication (application);
            }
        } else {
            factory = new SimpleKeyFetcherFactory ();
        }

        if (e.hasAttribute ("provider")) {
            String className = e.getAttribute ("provider");
            Class.forName (className);
            System.setProperty ("org.dreamwork.secure.provider", className);
        }
        context.setFetcher (factory.getKeyFetcher ());

        NodeList list = e.getElementsByTagName ("algorithm");
        if (list != null && list.getLength () > 0) for (int i = 0; i < list.getLength (); i ++) {
            Element a = (Element) list.item (i);
            String use = a.getAttribute ("use").trim ();
            String schema = a.getAttribute ("schema").trim ();
            AlgorithmMapping algorithm = AlgorithmMapping.find (schema);
            if (logger.isDebugEnabled ()) {
                logger.debug ("[secure context] use for=" + use + ", schema=" + schema + ",jce=" + algorithm.jceName);
            }
            if ("key-wrap".equals (use)) {
                context.setKeyTransport (algorithm);
            } else if ("block-encryption".equals (use)) {
                context.setBlockEncryption (algorithm);
            } else if ("signature".equals (use)) {
                context.setSignature (algorithm);
            }
        }
        RestfulAPIContext.secureContext = context;
    }
}